﻿using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Text;
using System.Threading.Tasks;

namespace Applay
{
    internal class Proxy : RealProxy
    {
        private readonly Wrapper wrapper;

        private readonly Service service;

        public Proxy(Wrapper wrapper, Service service) : base(service.GetType())
        {
            Contract.Requires(wrapper != null);
            Contract.Requires(service != null);

            this.wrapper = wrapper;
            this.service = service;
            this.service.Initialize(wrapper);
        }

        public override IMessage Invoke(IMessage msg)
        {
            // arrange
            var mcm = (IMethodCallMessage)msg;
            var serviceType = service.GetType();
            var arguments = (object[])mcm.Args.Clone();
            var methodInfo = serviceType.GetMethod(mcm.MethodName, (Type[])mcm.MethodSignature);

            // authorize
            this.Authorize(methodInfo);

            // begin validation scope
            bool disposeValidationScope = false;
            if (this.wrapper.ValidationScope == null)
            {
                this.wrapper.BeginValidation();
                disposeValidationScope = true;
            }

            // validate
            var validationResults = new List<ValidationResult>();
            this.Validate(mcm, methodInfo, validationResults);
            
            if (validationResults.Count > 0)
            {
                var validationState = this.wrapper.ValidationScope.ValidationState;

                foreach (var validationResult in validationResults)
                {
                    validationState.AddError(validationResult.ErrorMessage, validationResult.MemberNames.ToArray());
                }
            }

            // invoke
            var result = this.Invoke(arguments, methodInfo);

            // dispose validation scope
            if (disposeValidationScope)
            {
                this.wrapper.ValidationScope.Dispose();
            }

            return new ReturnMessage(result, arguments, arguments.Count(), mcm.LogicalCallContext, mcm);
        }

        private object Invoke(object[] arguments, MethodInfo methodInfo)
        {
            Contract.Requires(methodInfo != null);

            return methodInfo.Invoke(this.service, arguments);
        }

        private void Authorize(MethodInfo methodInfo)
        {
            Contract.Requires(methodInfo != null);

            if (this.wrapper.getPrincipal == null)
            {
                return;
            }

            var principal = this.wrapper.getPrincipal();
            var context = new AuthorizationContext(methodInfo, principal);

            foreach (var filter in AuthorizationFiltersFor(methodInfo))
            {
                filter.OnAuthorize(context);
            }

            if (!context.IsAuthorized)
            {
                throw new UnauthorizedException();
            }
        }

        private IEnumerable<IAuthorizationFilter> AuthorizationFiltersFor(MethodInfo methodInfo)
        {
            var filters = this.wrapper.Filters.OfType<IAuthorizationFilter>().ToList();

            filters.AddRange(methodInfo.DeclaringType.GetCustomAttributes(true).OfType<IAuthorizationFilter>());

            filters.AddRange(methodInfo.GetCustomAttributes(true).OfType<IAuthorizationFilter>());

            return filters;
        }

        private void Validate(IMethodCallMessage mcm, MethodInfo methodInfo, ICollection<ValidationResult> validationResults)
        {
            var arguments = mcm.InArgs;
            var parameters = methodInfo.GetParameters().Where(p => !p.IsOut).ToArray();

            for (var i = 0; i < arguments.Count(); i++)
            {
                var argument = arguments[i];
                var parameterInfo = parameters[i];

                ValidateArgument(argument, parameterInfo, validationResults);

                if (argument != null)
                {
                    Validator.TryValidateObject(argument, new ValidationContext(argument), validationResults, true);
                }
            }
        }

        private static void ValidateArgument(object argument, ParameterInfo parameterInfo, ICollection<ValidationResult> validationResults)
        {
            var context = new ValidationContext(argument ?? new object());
            context.DisplayName = DisplayNameFrom(parameterInfo);
            var validationAttributes = parameterInfo.GetCustomAttributes<ValidationAttribute>(true);
            Validator.TryValidateValue(argument, context, validationResults, validationAttributes);
        }

        private static string DisplayNameFrom(ParameterInfo parameterInfo)
        {
            Contract.Requires(parameterInfo != null);

            var displayAttribute = parameterInfo.GetCustomAttribute<DisplayAttribute>(true);

            if (displayAttribute == null)
            {
                return parameterInfo.Name.First().ToString().ToUpper() + parameterInfo.Name.Substring(1);
            }

            return displayAttribute.GetName();
        }
    }
}
